import json
import logging
import os
import pathlib
import pprint
import re
import subprocess
import sys
import tarfile
import textwrap
import time

import attr
import requests
import yaml

try:
    import crypt

    HAS_CRYPT = True
except ImportError:
    HAS_CRYPT = False
try:
    import pwd

    HAS_PWD = True
except ImportError:
    HAS_PWD = False

from zipfile import ZipFile

TESTS_DIR = pathlib.Path(__file__).resolve().parent.parent
CODE_DIR = TESTS_DIR.parent
ARTIFACTS_DIR = CODE_DIR / "artifacts"

log = logging.getLogger(__name__)


def run(cmd, minion="local"):
    if not isinstance(cmd, list):
        raise TypeError("Expected cmd to be a list")
    ret = {}
    proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    ret["stdout"] = proc.stdout.decode()
    ret["stderr"] = proc.stderr.decode()
    if ret["stdout"]:
        try:
            ret["stdout"] = json.loads(ret["stdout"])[minion]
        except json.decoder.JSONDecodeError:
            pass
    ret["retcode"] = proc.returncode
    return ret


def api(data, username="saltdev", password="P@ssW0rd"):
    """
    Run salt-api cmds
    """
    api_uri = "http://localhost:8000"
    session = requests.Session()
    auth = {"username": username, "password": password, "eauth": "auto", "out": "json"}
    data = {**auth, **data}

    time.sleep(90)  # Sleep so singlebin on Windows can unzip and start the service

    resp = session.post(f"{api_uri}/run", data=data).json()

    minion = next(iter(resp["return"][0]))
    return resp["return"][0][minion]


### manage configs ###


def write_config(config, contents=None):
    """
    write to a config file
    """
    if not contents:
        contents = {}
    if sys.platform.startswith("win"):
        conf = pathlib.Path("C:/salt/conf", config)
    else:
        conf = pathlib.Path("/etc/salt/", config)
    conf.parent.mkdir(parents=True, exist_ok=True)
    with open(conf, "w") as _fh:
        yaml.safe_dump(contents, _fh)


### manage services ###


def systemd_conf(service, binary):
    contents = textwrap.dedent(
        """\
                [Unit]
                Description={service}

                [Service]
                KillMode=process
                Type=notify
                NotifyAccess=all
                LimitNOFILE=8192
                ExecStart={tgt}

                [Install]
                WantedBy=multi-user.target
                """
    )
    if isinstance(binary, list):
        binary = " ".join(binary)
    with open(
        pathlib.Path(os.sep, "etc", "systemd", "system", f"{service}.service"), "w+"
    ) as wfp:
        wfp.write(
            contents.format(
                service=service,
                tgt=binary,
            )
        )


def remove_systemd_conf(service):
    pathlib.Path.unlink(
        pathlib.Path(os.sep, "etc", "systemd", "system", f"{service}.service")
    )


def start_service(service, binary, compressed=False):
    if sys.platform.startswith("win"):
        start = run(["net", "start", service])
        assert start["retcode"] == 0
    else:
        if compressed:
            systemd_conf(service, binary)

        # todo: verify service is up instead of time.sleep
        # reload first
        run(["systemctl", "daemon-reload"])
        start = run(["systemctl", "start", service])
        assert start["retcode"] == 0
        time.sleep(5)
        status = run(["systemctl", "status", service])
        log.debug(f"Systemd service status for {service}:\n{pprint.pformat(status)}")


def stop_service(service, compressed=False):

    if sys.platform.startswith("win"):
        stop = run(["net", "stop", service])
        assert stop["retcode"] == 0
    else:
        # todo: verify service is down
        stop = run(["systemctl", "stop", service])
        assert stop["retcode"] == 0

        if compressed:
            remove_systemd_conf(service)
            run(["systemctl", "daemon-reload"])


class Pkg:
    def __init__(self):
        self.pkgs = []
        self.onedir = False
        self.singlebin = False
        self.compressed = False
        self.hashes = {
            "BLAKE2B": {"file": None, "tool": "-blake2b512"},
            "SHA3_512": {"file": None, "tool": "-sha3-512"},
            "SHA512": {"file": None, "tool": "-sha512"},
        }

        file_ext = "tar.gz"
        if sys.platform.startswith("win"):
            file_ext = "zip"
        for files in ARTIFACTS_DIR.glob("**/*.*"):
            files = str(files)
            if re.search(f"salt-(.*).({file_ext})", files):
                self.compressed = True
                self.pkgs.append(files)
                if sys.platform.startswith("win"):
                    self.root = pathlib.Path(os.getenv("LocalAppData"))

                    with ZipFile(files, "r") as zip:
                        first = zip.infolist()[0]
                        if first.filename == "salt/ssm.exe":
                            self.onedir = True
                            self.run_root = self.root / "salt" / "salt" / "salt.exe"
                            self.ssm_bin = self.root / "salt" / "ssm.exe"
                        elif first.filename == "salt.exe":
                            self.singlebin = True
                            self.run_root = self.root / "salt.exe"
                            self.ssm_bin = self.root / "ssm.exe"
                        else:
                            log.error(
                                "Unexpected archive layout:\n"
                                f"first: {first.filename}\n"
                            )
                else:
                    self.root = pathlib.Path(os.sep) / "usr" / "local" / "bin"

                    with tarfile.open(files) as tar:
                        # The first item will be called salt
                        first = tar.getmembers()[0]
                        if first.name == "salt" and first.isdir():
                            self.onedir = True
                            self.run_root = self.root / "salt" / "run" / "run"
                        elif first.name == "salt" and first.isfile():
                            self.singlebin = True
                            self.run_root = self.root / "salt"
                        else:
                            log.error(
                                "Unexpected archive layout:\n"
                                f"first: {first.name}\n"
                                f"isdir: {first.isdir()}\n"
                                f"isfile: {first.isfile()}"
                            )

            if re.search("salt(.*)(x86_64|all|amd64).(rpm|deb)", files):
                self.pkgs.append(files)

        if not self.pkgs:
            log.error("Could not find Salt Artifacts")

    @property
    def salt_hashes(self):
        for _hash in self.hashes.keys():
            for files in ARTIFACTS_DIR.glob(f"**/*{_hash}*"):
                files = str(files)
                if re.search(f"{_hash}", files):
                    self.hashes[_hash]["file"] = files

        return self.hashes

    @property
    def salt_bin(self):
        if not self.compressed:
            salt_bin = {
                "salt": ["salt"],
                "api": ["salt-api"],
                "call": ["salt-call"],
                "cloud": ["salt-cloud"],
                "cp": ["salt-cp"],
                "key": ["salt-key"],
                "master": ["salt-master"],
                "minion": ["salt-minion"],
                "proxy": ["salt-proxy"],
                "run": ["salt-run"],
                "ssh": ["salt-ssh"],
                "syndic": ["salt-syndic"],
                "spm": ["spm"],
                "pip": ["salt-pip"],
            }
        if self.compressed:
            salt_bin = {
                "salt": [str(self.run_root)],
                "api": [str(self.run_root), "api"],
                "call": [str(self.run_root), "call"],
                "cloud": [str(self.run_root), "cloud"],
                "cp": [str(self.run_root), "cp"],
                "key": [str(self.run_root), "key"],
                "master": [str(self.run_root), "master"],
                "minion": [str(self.run_root), "minion"],
                "proxy": [str(self.run_root), "proxy"],
                "run": [str(self.run_root), "run"],
                "ssh": [str(self.run_root), "ssh"],
                "syndic": [str(self.run_root), "syndic"],
                "spm": [str(self.run_root), "spm"],
                "pip": [str(self.run_root), "pip"],
            }

        return salt_bin

    def salt_call_local(self, args):
        """
        Run salt-call --local commands
        """
        cmd = self.salt_bin["call"] + ["--local", "--out=json"]
        cmd += args
        ret = run(cmd)
        return ret

    def salt_call(self, args):
        """
        Run salt-call commands
        """
        cmd = self.salt_bin["call"] + ["--out=json"]
        cmd += args
        ret = run(cmd)
        return ret

    def salt_key(self, args):
        """
        Run salt-key commands
        """
        cmd = self.salt_bin["key"]
        cmd += args
        ret = run(cmd, minion="pkg_tests")
        return ret

    def salt_minion(self, args):
        """
        Run salt commands
        """
        cmd = ["salt", "*"]
        cmd = self.salt_bin["salt"] + ["*"]
        cmd += args
        cmd.append("--out=json")
        ret = run(cmd, minion="pkg_tests")
        return ret

    def salt_master(self, args):
        """
        Run salt commands
        """
        cmd = ["salt"]
        cmd = self.salt_bin["salt"]
        cmd += args
        cmd.append("--out=json")
        ret = run(cmd)
        return ret


@attr.s
class TestUser(Pkg):
    """
    Add a test user
    """

    username = attr.ib(default="saltdev")
    # Must follow Windows Password Complexity requirements
    password = attr.ib(default="P@ssW0rd")
    _pw_record = attr.ib(init=False, repr=False, default=None)

    def __attrs_post_init__(self):
        super().__init__()

    def add_user(self):
        log.debug("Adding system account %r", self.username)
        if sys.platform.startswith("win"):
            assert self.salt_call_local(["user.add", self.username, self.password])
        else:
            assert self.salt_call_local(["user.add", self.username])
            hash_passwd = crypt.crypt(self.password, crypt.mksalt(crypt.METHOD_SHA512))
            assert self.salt_call_local(
                ["shadow.set_password", self.username, hash_passwd]
            )
        assert self.username in self.salt_call_local(["user.list_users"])["stdout"]

    def remove_user(self):
        log.debug("Removing system account %r", self.username)
        if sys.platform.startswith("win"):
            assert self.salt_call_local(
                ["user.delete", self.username, "purge=True", "force=True"]
            )
        else:
            assert self.salt_call_local(["user.delete", self.username, "remove=True"])

    @property
    def pw_record(self):
        if self._pw_record is None:
            self._pw_record = pwd.getpwnam(self.username)
        return self._pw_record

    @property
    def uid(self):
        return self.pw_record.pw_uid

    @property
    def gid(self):
        return self.pw_record.pw_gid

    @property
    def env(self):
        environ = os.environ.copy()
        environ["LOGNAME"] = environ["USER"] = self.username
        environ["HOME"] = self.pw_record.pw_dir
        return environ
